diff --git a/cypress/integration/api-tokens.spec.js b/cypress/integration/api-tokens.spec.js
--- a/cypress/integration/api-tokens.spec.js
+++ b/cypress/integration/api-tokens.spec.js
@@ -8,7 +8,7 @@
 describe('Test API tokens UI', function() {
 
   it('should ask for user to login', function() {
-    cy.visit(this.Urls.api_tokens(), {failOnStatusCode: false});
+    cy.visit(`${this.Urls.oidc_profile()}#tokens`, {failOnStatusCode: false});
     cy.location().should(loc => {
       expect(loc.pathname).to.eq(this.Urls.oidc_login());
     });
@@ -29,7 +29,7 @@
     // the tested UI should not be accessible for standard Django users
     // but we need a user logged in for testing it
     cy.adminLogin();
-    cy.visit(Urls.api_tokens());
+    cy.visit(`${Urls.oidc_profile()}#tokens`);
   }
 
   function generateToken(Urls, status, tokenValue = '') {
diff --git a/swh/web/api/urls.py b/swh/web/api/urls.py
--- a/swh/web/api/urls.py
+++ b/swh/web/api/urls.py
@@ -3,10 +3,6 @@
 # License: GNU Affero General Public License version 3, or any later version
 # See top-level LICENSE file for more information
 
-from django.conf.urls import url
-from django.contrib.auth.decorators import login_required
-from django.shortcuts import render
-
 from swh.web.api.apiurls import APIUrls
 import swh.web.api.views.content  # noqa
 import swh.web.api.views.directory  # noqa
@@ -21,11 +17,4 @@
 import swh.web.api.views.stat  # noqa
 import swh.web.api.views.vault  # noqa
 
-
-@login_required(login_url="/oidc/login/", redirect_field_name="next_path")
-def _tokens_view(request):
-    return render(request, "api/tokens.html")
-
-
 urlpatterns = APIUrls.get_url_patterns()
-urlpatterns.append(url(r"^tokens/$", _tokens_view, name="api-tokens"))
diff --git a/swh/web/assets/src/bundles/auth/index.js b/swh/web/assets/src/bundles/auth/index.js
--- a/swh/web/assets/src/bundles/auth/index.js
+++ b/swh/web/assets/src/bundles/auth/index.js
@@ -169,7 +169,7 @@
   });
 }
 
-export function initApiTokensPage() {
+export function initProfilePage() {
   $(document).ready(() => {
     apiTokensTable = $('#swh-bearer-tokens-table')
       .on('error.dt', (e, settings, techNote, message) => {
@@ -212,5 +212,12 @@
         scrollY: '50vh',
         scrollCollapse: true
       });
+    $('#swh-oidc-profile-tokens-tab').on('shown.bs.tab', () => {
+      apiTokensTable.draw();
+      window.location.hash = '#tokens';
+    });
+    if (window.location.hash === '#tokens') {
+      $('.nav-tabs a[href="#swh-oidc-profile-tokens"]').tab('show');
+    }
   });
 }
diff --git a/swh/web/auth/views.py b/swh/web/auth/views.py
--- a/swh/web/auth/views.py
+++ b/swh/web/auth/views.py
@@ -13,6 +13,7 @@
 
 from django.conf.urls import url
 from django.contrib.auth import authenticate, login, logout
+from django.contrib.auth.decorators import login_required
 from django.core.cache import cache
 from django.core.paginator import Paginator
 from django.http import HttpRequest
@@ -23,6 +24,7 @@
     HttpResponseServerError,
     JsonResponse,
 )
+from django.shortcuts import render
 from django.views.decorators.http import require_http_methods
 
 from swh.web.auth.models import OIDCUser, OIDCUserOfflineTokens
@@ -216,6 +218,11 @@
         return HttpResponse(status=401)
 
 
+@login_required(login_url="/oidc/login/", redirect_field_name="next_path")
+def _oidc_profile_view(request: HttpRequest) -> HttpResponse:
+    return render(request, "auth/profile.html")
+
+
 urlpatterns = [
     url(r"^oidc/login/$", oidc_login, name="oidc-login"),
     url(r"^oidc/login-complete/$", oidc_login_complete, name="oidc-login-complete"),
@@ -240,4 +247,5 @@
         oidc_revoke_bearer_tokens,
         name="oidc-revoke-bearer-tokens",
     ),
+    url(r"^oidc/profile/$", _oidc_profile_view, name="oidc-profile",),
 ]
diff --git a/swh/web/common/utils.py b/swh/web/common/utils.py
--- a/swh/web/common/utils.py
+++ b/swh/web/common/utils.py
@@ -260,6 +260,7 @@
         "swh_client_config": config["client_config"],
         "oidc_enabled": bool(config["keycloak"]["server_url"]),
         "browsers_supported_image_mimes": browsers_supported_image_mimes,
+        "keycloak": config["keycloak"],
     }
 
 
diff --git a/swh/web/templates/api/tokens.html b/swh/web/templates/api/tokens.html
deleted file mode 100644
--- a/swh/web/templates/api/tokens.html
+++ /dev/null
@@ -1,57 +0,0 @@
-{% extends "layout.html" %}
-
-{% comment %}
-Copyright (C) 2020  The Software Heritage developers
-See the AUTHORS file at the top-level directory of this distribution
-License: GNU Affero General Public License version 3, or any later version
-See top-level LICENSE file for more information
-{% endcomment %}
-
-{% load render_bundle from webpack_loader %}
-{% load swh_templatetags %}
-
-{% block title %} Web API bearer tokens – Software Heritage API {% endblock %}
-
-{% block header %}
-{% render_bundle 'auth' %}
-{% endblock %}
-
-{% block navbar-content %}
-<h4>Web API bearer tokens management</h4>
-{% endblock %}
-
-{% block content %}
-
-<p>
-  That interface enables to manage bearer tokens for Web API authentication.
-  A token has to be sent in HTTP authorization headers to make authenticated API requests.
-</p>
-<p>
-  For instance when using <code>curl</code> proceed as follows:
-  <pre>curl -H "Authorization: Bearer ${TOKEN}" {{ request.scheme }}://{{ request.META.HTTP_HOST }}/api/...</pre>
-</p>
-
-<div class="mt-3">
-  <div class="float-right">
-    <button class="btn btn-default" onclick="swh.auth.applyTokenAction('generate')">
-      Generate new token
-    </button>
-    <button class="btn btn-default float-right" onclick="swh.auth.applyTokenAction('revokeAll')">
-      Revoke all tokens
-    </button>
-  </div>
-  <table id="swh-bearer-tokens-table" class="table swh-table swh-table-striped" width="100%">
-    <thead>
-      <tr>
-        <th>Creation date</th>
-        <th>Actions</th>
-      </tr>
-    </thead>
-  </table>
-</div>
-
-<script>
-  swh.auth.initApiTokensPage();
-</script>
-
-{% endblock content %}
\ No newline at end of file
diff --git a/swh/web/templates/auth/profile.html b/swh/web/templates/auth/profile.html
new file mode 100644
--- /dev/null
+++ b/swh/web/templates/auth/profile.html
@@ -0,0 +1,74 @@
+{% extends "layout.html" %}
+
+{% comment %}
+Copyright (C) 2020  The Software Heritage developers
+See the AUTHORS file at the top-level directory of this distribution
+License: GNU Affero General Public License version 3, or any later version
+See top-level LICENSE file for more information
+{% endcomment %}
+
+{% load render_bundle from webpack_loader %}
+{% load swh_templatetags %}
+
+{% block title %} User profile &ndash; Software Heritage {% endblock %}
+
+{% block header %}
+{% render_bundle 'auth' %}
+{% endblock %}
+
+{% block navbar-content %}
+<h4>User profile</h4>
+{% endblock %}
+
+{% block content %}
+
+<ul class="nav nav-tabs" style="padding-left: 5px;">
+  <li class="nav-item">
+    <a class="nav-link active" data-toggle="tab" id="swh-oidc-profile-settings-tab" href="#swh-oidc-profile-settings">Settings</a>
+  </li>
+  <li class="nav-item">
+    <a class="nav-link" data-toggle="tab" id="swh-oidc-profile-tokens-tab" href="#swh-oidc-profile-tokens">API tokens</a>
+  </li>
+</ul>
+
+<div class="tab-content">
+  <div id="swh-oidc-profile-settings" class="tab-pane active">
+    <iframe id="swh-oidc-profile-settings-iframe" style="min-width: 100%; height: 90vh; border: none;"
+            src="{{ keycloak.server_url }}realms/{{ keycloak.realm_name }}/account/?swh-web-user-profile"></iframe>
+  </div>
+
+  <div id="swh-oidc-profile-tokens" class="tab-pane">
+    <p class="mt-3">
+      That interface enables to manage bearer tokens for Web API authentication.
+      A token has to be sent in HTTP authorization headers to make authenticated API requests.
+    </p>
+    <p>
+      For instance when using <code>curl</code> proceed as follows:
+      <pre>curl -H "Authorization: Bearer ${TOKEN}" {{ request.scheme }}://{{ request.META.HTTP_HOST }}/api/...</pre>
+    </p>
+    <div class="mt-3">
+      <div class="float-right">
+        <button class="btn btn-default" onclick="swh.auth.applyTokenAction('generate')">
+          Generate new token
+        </button>
+        <button class="btn btn-default float-right" onclick="swh.auth.applyTokenAction('revokeAll')">
+          Revoke all tokens
+        </button>
+      </div>
+      <table id="swh-bearer-tokens-table" class="table swh-table swh-table-striped" width="100%">
+        <thead>
+          <tr>
+            <th>Creation date</th>
+            <th>Actions</th>
+          </tr>
+        </thead>
+      </table>
+    </div>
+  </div>
+</div>
+
+<script>
+  swh.auth.initProfilePage();
+</script>
+
+{% endblock content %}
diff --git a/swh/web/templates/layout.html b/swh/web/templates/layout.html
--- a/swh/web/templates/layout.html
+++ b/swh/web/templates/layout.html
@@ -1,5 +1,5 @@
 {% comment %}
-Copyright (C) 2015-2019  The Software Heritage developers
+Copyright (C) 2015-2020  The Software Heritage developers
 See the AUTHORS file at the top-level directory of this distribution
 License: GNU Affero General Public License version 3, or any later version
 See top-level LICENSE file for more information
@@ -26,7 +26,7 @@
 /*
 @licstart  The following is the entire license notice for the JavaScript code in this page.
 
-Copyright (C) 2015-2019  The Software Heritage developers
+Copyright (C) 2015-2020  The Software Heritage developers
 
 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
@@ -92,10 +92,12 @@
           <li class="swh-position-right">
             {% url 'logout' as logout_url %}
             {% if user.is_authenticated %}
-              Logged in as <strong>{{ user.username }}</strong>,
+              Logged in as
               {% if 'OIDC' in user.backend %}
+                <a href="{% url 'oidc-profile' %}"><strong>{{ user.username }}</strong></a>,
                 <a href="{% url 'oidc-logout' %}">logout</a>
               {% else %}
+                <strong>{{ user.username }}</strong>,
                 <a href="{{ logout_url }}">logout</a>
               {% endif %}
             {% elif oidc_enabled %}
diff --git a/swh/web/tests/auth/test_views.py b/swh/web/tests/auth/test_views.py
--- a/swh/web/tests/auth/test_views.py
+++ b/swh/web/tests/auth/test_views.py
@@ -16,6 +16,7 @@
 from swh.web.auth.models import OIDCUser, OIDCUserOfflineTokens
 from swh.web.auth.utils import OIDC_SWH_WEB_CLIENT_ID
 from swh.web.common.utils import reverse
+from swh.web.config import get_config
 from swh.web.tests.django_asserts import assert_contains
 from swh.web.tests.utils import (
     check_html_get_response,
@@ -525,3 +526,33 @@
         status_code=401,
         data={"password": "invalid-password", "token_ids": [1]},
     )
+
+
+def test_oidc_profile_view_anonymous_user(client):
+    """
+    Non authenticated users should be redirected to login page when
+    requesting profile view.
+    """
+    url = reverse("oidc-profile")
+    login_url = reverse("oidc-login", query_params={"next_path": url})
+    resp = check_html_get_response(client, url, status_code=302)
+    assert resp["location"] == login_url
+
+
+@pytest.mark.django_db
+def test_oidc_profile_view(client, mocker):
+    """
+    Authenticated users should be able to request the profile page
+    and link to Keycloak account UI should be present.
+    """
+    url = reverse("oidc-profile")
+    kc_config = get_config()["keycloak"]
+    mock_keycloak(mocker)
+    client.login(code="", code_verifier="", redirect_uri="")
+    resp = check_html_get_response(
+        client, url, status_code=200, template_used="auth/profile.html"
+    )
+    kc_account_url = (
+        f"{kc_config['server_url']}realms/{kc_config['realm_name']}/account/"
+    )
+    assert_contains(resp, kc_account_url)